<template>
  <div class="workflow-canvas">
    <!-- 画布工具栏 -->
    <div class="canvas-toolbar">
      <div class="toolbar-left">
        <el-button size="small" @click="handleUndo" :disabled="!canUndo">
          <el-icon>
            <RefreshLeft/>
          </el-icon>
          撤销
        </el-button>
        <el-button size="small" @click="handleRedo" :disabled="!canRedo">
          <el-icon>
            <RefreshRight/>
          </el-icon>
          重做
        </el-button>
      </div>

      <div class="toolbar-center">
        <el-button size="small" @click="handleZoomOut">
          <el-icon>
            <ZoomOut/>
          </el-icon>
        </el-button>
        <span class="zoom-level">{{ Math.round(zoom * 100) }}%</span>
        <el-button size="small" @click="handleZoomIn">
          <el-icon>
            <ZoomIn/>
          </el-icon>
        </el-button>
        <el-button size="small" @click="handleFitView">
          适应画布
        </el-button>
        <el-button size="small" @click="handleCenter">
          居中
        </el-button>
      </div>

      <div class="toolbar-right">
        <el-button size="small" type="primary" @click="handleSave">
          <el-icon>
            <Check/>
          </el-icon>
          保存
        </el-button>
      </div>
    </div>

    <!-- X6画布容器 -->
    <div ref="container" class="x6-container"></div>
  </div>
</template>

<script>
import {defineComponent, ref, onMounted, onUnmounted, watch} from 'vue'
import {Graph} from '@antv/x6'
import {Transform} from '@antv/x6-plugin-transform'
import {Selection} from '@antv/x6-plugin-selection'
import {Keyboard} from '@antv/x6-plugin-keyboard'
import {Clipboard} from '@antv/x6-plugin-clipboard'
import {History} from '@antv/x6-plugin-history'
import {nodeFactory} from '@/components/WorkFlow/core/NodeFactory'
import {edgeFactory} from '@/components/WorkFlow/core/EdgeFactory'
import {ElMessage} from 'element-plus'
import {
  RefreshLeft, RefreshRight, ZoomIn, ZoomOut, Check
} from '@element-plus/icons-vue'

/**
 * 工作流画布组件
 * 基于X6的工作流图形编辑画布
 */
export default defineComponent({
  name: 'WorkflowCanvas',

  components: {
    RefreshLeft, RefreshRight, ZoomIn, ZoomOut, Check
  },

  props: {
    workflowData: {
      type: Object,
      default: () => ({})
    }
  },

  setup(props, {emit}) {
    const container = ref(null)
    const graph = ref(null)
    const zoom = ref(1)
    const canUndo = ref(false)
    const canRedo = ref(false)
    let isReverting = false
    let allowLoopMove = true
    let currentDraggingLoopId = null
    const dragStart = new Map()

    // 端口与连线颜色配置（借鉴 demo 风格）
    const PORT_DOT_RADIUS = 3
    const COLOR_PORT_GRAY = '#C2C8D5'
    const COLOR_PORT_BLUE = '#5F95FF'
    const COLOR_EDGE_TOOL_BLUE = '#2871CE'

    /**
     * 初始化 X6 图形与插件
     * @returns {void}
     *
     * 配置说明：
     * - background/grid：背景与网格样式
     * - panning/mousewheel：画布平移与滚轮缩放（需要按修饰键）
     * - translating.restrict：当节点有父泳道时，限制移动在泳道内容区（排除标题栏，留内边距）
     * - connecting：连线相关配置
     *   - connectionPoint: 使用 boundary，并设置 padding=3，避免边线侵入连接桩
     *   - allowMulti: 允许同一端口引出多条边
     *   - createEdge: 自定义拖拽预览边的样式（缩小箭头尺寸）
     *   - validateConnection: 只有两端在同一泳道才允许连线（或都不在泳道）
     * - embedding.findParent：查找可嵌入父节点（仅返回泳道数组）
     * - highlighting/interacting：高亮与交互能力开关
     */
    const initGraph = () => {
      if (!container.value) return

      graph.value = new Graph({
        container: container.value,
        width: '100%',
        height: '100%',
        background: { color: '#f6f3f3' },
        grid: { visible: true, type: 'dot', args: { color: '#e0e0e0' } },
        // 仅在按住空格键时允许平移画布，避免误触发
        panning: {
          enabled: true,
          modifiers: 'space'
        },
        mousewheel: {
          enabled: true,
          modifiers: ['ctrl', 'meta']
        },
        /**
         * 限制子节点的拖动范围：当节点属于某个循环泳道时，仅允许在父泳道的包围盒内移动
         * @param {import('@antv/x6').NodeView} view - 当前视图
         * @returns {import('@antv/x6').Rectangle|null} 允许移动的矩形范围
         */
        translating: {
          restrict(view) {
            const cell = view.cell
            if (cell && cell.isNode()) {
              const parent = cell.getParent()
              if (parent && (parent.getData()?.type) === 'loop') {
                const pb = parent.getBBox()
                const hh = parent.getData()?.headerHeight ?? 30
                const pad = parent.getData()?.innerPadding ?? 8
                const y = pb.y + hh + pad
                const h = Math.max(0, pb.height - hh - pad * 2)
                const x = pb.x + pad
                const w = Math.max(0, pb.width - pad * 2)
                return {x, y, width: w, height: h}
              }
            }
            return null
          }
        },
        connecting: {
          connector: {name: 'smooth'},
          connectionPoint: {name: 'boundary', args: {padding: 3}},
          allowBlank: false,
          allowEdge: false,
          allowLoop: false,
          allowMulti: true,
          highlight: true,
          snap: {radius: 30},
          /**
           * 自定义拖拽预览边
           * @returns {import('@antv/x6').Edge} 预览边实例
           *
           * 说明：将箭头 targetMarker 的尺寸调小，减少遮挡与侵入端口视觉的概率。
           */
          createEdge() {
            return graph.value.createEdge({
              shape: 'edge',
              attrs: {
                line: {
                  stroke: COLOR_EDGE_TOOL_BLUE,
                  targetMarker: {
                    name: 'block',
                    size: 14,//边上箭头的大小
                    fill: COLOR_EDGE_TOOL_BLUE,
                    stroke: COLOR_EDGE_TOOL_BLUE
                  }
                }
              }
            })
          },
          /**
           * 验证连线合法性：
           * 1) 当源或目标节点属于某一循环泳道时，要求两端处于同一泳道内
           * 2) 结束节点（end）不允许作为连线源
           * 3) 结束节点（end）作为目标时，仅允许一个入边
           * @param {Object} args - 连接参数对象
           * @returns {boolean} 是否允许连接
           */
          validateConnection(args) {
            const {sourceCell, targetCell} = args
            if (!sourceCell || !targetCell) return false
            const sData = sourceCell.getData() || {}
            const tData = targetCell.getData() || {}
            // 结束节点不能作为源
            if (sData.type === 'end') return false
            // 结束节点仅允许一个入边
            if (tData.type === 'end') {
              try {
                const incoming = (graph.value?.getConnectedEdges?.(targetCell) || []).filter(e => e.getTargetCellId?.() === targetCell.id)
                if (incoming.length >= 1) return false
              } catch (e) {}
            }
            const sParent = sData.parentId || null
            const tParent = tData.parentId || null
            if (!sParent && !tParent) return true
            return sParent && tParent && sParent === tParent
          }
        },
        /**
         * 启用嵌套：允许普通节点拖入循环泳道成为其子节点
         * 使用自定义父节点查找逻辑，避免嵌入非泳道节点
         */
        embedding: {
          enabled: true,
          /**
           * 查找可嵌入的父节点（循环泳道）
           * @param {{ node: import('@antv/x6').Node }} args - 包含子节点的参数对象
           * @returns {import('@antv/x6').Node|undefined} 父泳道节点
           */
          findParent({node}) {
            // 泳道本身不参与作为子节点的嵌套判定
            if ((node.getData()?.type) === 'loop') return []
            // 若节点已属于某个循环泳道，则在拖动过程中仅维持现有父级，不允许跨泳道重分配
            const currentParent = node.getParent?.() || null
            if (currentParent && (currentParent.getData()?.type) === 'loop') {
              return [currentParent]
            }
            const loops = graph.value.getNodes().filter(n => (n.getData()?.type) === 'loop')
            if (loops.length === 0) return []
            const bbox = node.getBBox()
            const center = {x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2}
            const candidates = []
            for (let i = 0; i < loops.length; i += 1) {
              const lb = loops[i].getBBox()
              if (center.x >= lb.x && center.x <= lb.x + lb.width &&
                  center.y >= lb.y && center.y <= lb.y + lb.height) {
                candidates.push(loops[i])
              }
            }
            return candidates
          }
        },
        highlighting: {
          magnetAdsorbed: {
            name: 'stroke',
            args: {
              attrs: {fill: COLOR_PORT_BLUE, stroke: COLOR_PORT_BLUE, strokeWidth: 2}
            }
          }
        },
        // 启用节点交互功能
        interacting: {
          nodeMovable: (view) => {
            const node = view.cell
            // 如果是循环节点，仅允许在标题栏拖动（由 allowLoopMove 状态控制）
            if (node.getData()?.type === 'loop') {
              return allowLoopMove
            }
            const parent = node.getParent()
            if (currentDraggingLoopId && parent && parent.id === currentDraggingLoopId) {
              return false
            }
            return true
          },
          edgeMovable: true,
          edgeLabelMovable: true,
          arrowheadMovable: true,
          vertexMovable: true,
          vertexAddable: true,
          vertexDeletable: true
        }
      })

      // 安装插件
      graph.value
          .use(new Transform({
            resizing: {
              enabled: (node) => node.data.type === "loop",
            },
            rotating: false, // 禁用旋转
            // 只允许单个节点变换
            boundary: true,
            // 限制变换操作只对单个节点有效
            modifiers: ['shift']
          }))
          .use(new Selection({
            // 仅在按住ctrl时框选，允许多选并整体拖动
            rubberband: true,
            multiple: true,
            movable: true,
            modifiers: ['shift']
          }))
          .use(new Keyboard())
          .use(new Clipboard())
          .use(new History({
            enabled: true
          }))

      // 绑定事件
      bindEvents()

      // 设置拖拽事件监听
      setupDragAndDrop()

      // 设置快捷键
      setupKeyboardShortcuts()

      // 初始缩放
      graph.value.zoomTo(0.8)
      zoom.value = 0.8
    }

    /**
     * 绑定画布交互事件
     * @returns {void}
     *
     * 事件说明：
     * - scale/history: 更新缩放值与撤销/重做状态
     * - node/edge contextmenu: 右键菜单事件
     * - blank click: 清除选中
     * - node mouseenter/leave: 端口显隐
     * - edge added/connected/removed: 端口着色与恢复
     * - node change:position: 拖动中维护父子关系并阻止外部节点进入泳道
     */
    const bindEvents = () => {
      if (!graph.value) return

      // 缩放事件
      graph.value.on('scale', ({sx}) => {
        zoom.value = sx
      })

      // 历史事件
      graph.value.on('history:change', () => {
        updateHistoryState()
      })

      // 节点右键菜单（修正定位到屏幕坐标）
      graph.value.on('node:contextmenu', ({node, x, y}) => {
        const p = graph.value.localToClient({x, y})
        emit('node-context-menu', node, {x: p.x, y: p.y})
      })

      // 边右键菜单（修正定位到屏幕坐标）
      graph.value.on('edge:contextmenu', ({edge, x, y}) => {
        const p = graph.value.localToClient({x, y})
        emit('edge-context-menu', edge, {x: p.x, y: p.y})
      })

      // 节点点击
      graph.value.on('node:click', ({node}) => {
        emit('node-click', node)
      })
      /**
       * 选中与取消选中时切换边框颜色
       * 默认绿色，选中后蓝色
       */
      graph.value.on('node:selected', ({node}) => {
        try {
          const isLoop = (node.getData()?.type) === 'loop'
          node.setAttrs({body: {stroke: 'rgb(131, 168, 250)', strokeWidth: isLoop ? 3 : 2}})
          // 选中节点后打开配置抽屉
          emit('node-selected', node)
        } catch (e) {
        }
      })
      graph.value.on('node:unselected', ({node}) => {
        try {
          const isLoop = (node.getData()?.type) === 'loop'
          node.setAttrs({body: {stroke: isLoop ? '#FFF' : 'rgb(137, 210, 178)', strokeWidth: isLoop ? 8 : 2}})
        } catch (e) {
        }
      })

      // 边点击
      graph.value.on('edge:click', ({edge}) => {
        emit('edge-click', edge)
      })

      /**
       * 启用边工具：点击边后展示可编辑工具，避免遮挡端口
       */
      graph.value.on('edge:click', ({edge}) => {
        try {
          edge.addTools([
            {name: 'vertices', args: {attrs: {fill: COLOR_EDGE_TOOL_BLUE, stroke: COLOR_EDGE_TOOL_BLUE}}},
            {name: 'source-arrowhead', args: {attrs: {fill: COLOR_EDGE_TOOL_BLUE, stroke: COLOR_EDGE_TOOL_BLUE}}},
            {name: 'target-arrowhead', args: {attrs: {fill: COLOR_EDGE_TOOL_BLUE, stroke: COLOR_EDGE_TOOL_BLUE}}}
          ])
          // 选中边时，线与箭头统一高亮为蓝色，但保持箭头样式不变
          edge.setAttrs({
            line: {
              stroke: COLOR_EDGE_TOOL_BLUE,
              strokeWidth: 2,
              targetMarker: {name: 'classic', size: 12, fill: COLOR_EDGE_TOOL_BLUE, stroke: COLOR_EDGE_TOOL_BLUE}
            }
          })
          // 两端连接桩也高亮为蓝色
          withNodePort(edge.getSourceCellId(), edge.getSourcePortId(), (node, port) => setPortColor(node, port, COLOR_EDGE_TOOL_BLUE))
          withNodePort(edge.getTargetCellId(), edge.getTargetPortId(), (node, port) => setPortColor(node, port, COLOR_EDGE_TOOL_BLUE))
        } catch (e) {
        }
      })
      graph.value.on('blank:click', () => {
        // 点击空白移除边工具（若存在）
        const edges = graph.value?.getEdges?.() || []
        edges.forEach(e => {
          try {
            e.removeTools()
            e.setAttrs({line: {stroke: COLOR_PORT_GRAY, strokeWidth: 1, targetMarker: {name: 'classic', size: 12, fill: COLOR_PORT_GRAY, stroke: COLOR_PORT_GRAY}}})
          } catch (err) {
          }
          try {
            withNodePort(e.getSourceCellId(), e.getSourcePortId(), (node, port) => setPortColor(node, port, COLOR_PORT_GRAY))
            withNodePort(e.getTargetCellId(), e.getTargetPortId(), (node, port) => setPortColor(node, port, COLOR_PORT_GRAY))
          } catch (err) {
          }
        })
      })

      // 画布点击
      graph.value.on('blank:click', () => {
        // 清除选中状态
        const selection = graph.value.getPlugin('selection')
        if (selection && selection.clearSelection) {
          selection.clearSelection()
        }
        // 通知父组件以关闭所有右键菜单
        emit('blank-click')
      })

      // 节点端口显隐（借鉴 demo 行为）
      graph.value.on('node:mouseenter', ({node}) => {
        showNodePorts(node, true)
      })
      graph.value.on('node:mouseleave', ({node}) => {
        showNodePorts(node, false)
      })

      // 边样式与端口上色
      graph.value.on('edge:added', ({edge}) => {
        edge.setAttrs({line: {stroke: COLOR_PORT_GRAY, strokeWidth: 1, targetMarker: {name: 'classic', size: 12, fill: COLOR_PORT_GRAY, stroke: COLOR_PORT_GRAY}}})
        withNodePort(edge.getSourceCellId(), edge.getSourcePortId(), (node, port) => setPortDot(node, port, true, COLOR_PORT_GRAY))
        withNodePort(edge.getTargetCellId(), edge.getTargetPortId(), (node, port) => setPortDot(node, port, true, COLOR_PORT_GRAY))
        try {
          const s = graph.value?.getCellById?.(edge.getSourceCellId?.())
          const t = graph.value?.getCellById?.(edge.getTargetCellId?.())
          if (s && s.getData?.()?.type === 'end') {
            try { graph.value.removeEdge(edge.id) } catch (e) {}
            try { ElMessage.warning('结束节点不允许作为连线源') } catch (e) {}
            return
          }
          if (t && t.getData?.()?.type === 'end') {
            const incoming = (graph.value?.getConnectedEdges?.(t) || []).filter(e => e.getTargetCellId?.() === t.id)
            if (incoming.length > 1) {
              try { graph.value.removeEdge(edge.id) } catch (e) {}
              try { ElMessage.warning('结束节点仅允许一个入边') } catch (e) {}
            }
          }
        } catch (e) {}
      })

      graph.value.on('edge:connected', ({currentCell, currentPort}) => {
        if (!currentCell || !currentPort) return
        setPortDot(currentCell, currentPort, true, COLOR_PORT_GRAY)
      })

      graph.value.on('edge:removed', ({edge}) => {
        withNodePort(edge.getSourceCellId(), edge.getSourcePortId(), (node, port) => {
          const still = isPortConnected(node, port)
          if (!still) setPortDot(node, port, false, COLOR_PORT_GRAY)
        })
        withNodePort(edge.getTargetCellId(), edge.getTargetPortId(), (node, port) => {
          const still = isPortConnected(node, port)
          if (!still) setPortDot(node, port, false, COLOR_PORT_GRAY)
        })
      })

      // 节点位置变化时，动态维护父子关系数据
      graph.value.on('node:change:position', ({node}) => {
        preventEnterLoopOnDrag(node)
        preventLoopIntersectOnDrag(node)
        preventLoopOverlapOnDrag(node)
        assignParentByGeometry(node, false)
      })

      // 节点大小变化时，动态更新循环节点的标签宽度
      graph.value.on('node:resized', ({node}) => {
        // 检查是否为循环节点
        if (node.getData()?.type === 'loop') {
          // 获取节点当前宽度
          const nodeWidth = node.getSize().width
          // 更新name-rect的宽度为节点宽度减去边框宽度
          if (nodeWidth > 0) {
            node.attr('name-rect', {
              refWidth: ((nodeWidth - 4) / nodeWidth) *100 + '%',
            })
          }
        }
      })

      // 记录拖拽开始位置与父子状态
      graph.value.on('node:mousedown', ({node, y}) => {
        if (node.getData()?.type === 'loop') {
          const bbox = node.getBBox()
          const headerHeight = node.getData()?.headerHeight ?? 30
          if (y > bbox.y + headerHeight) {
            allowLoopMove = false
            currentDraggingLoopId = null
          } else {
            allowLoopMove = true
            currentDraggingLoopId = node.id
          }
        } else {
          allowLoopMove = true
          currentDraggingLoopId = null
        }

        const pos = node.getPosition()
        // 记录当前节点的拖拽起点
        dragStart.set(node.id, {x: pos.x, y: pos.y, hadParent: !!node.getParent()})
        // 若有多选，补充记录所有选中节点的拖拽起点，以支持整体拖动时的父子关系校验
        const selected = graph.value.getSelectedCells()
        if (selected && selected.length) {
          selected.forEach(cell => {
            if (cell && cell.isNode && cell.isNode()) {
              const p = cell.getPosition()
              dragStart.set(cell.id, {x: p.x, y: p.y, hadParent: !!cell.getParent()})
            }
          })
        }
      })
      graph.value.on('node:mouseup', ({node}) => {
        dragStart.delete(node.id)
        currentDraggingLoopId = null
      })
    }

    /**
     * 设置常用快捷键（不影响既有功能）
     * - Ctrl + A 全选
     * - Ctrl + C 复制
     * - Ctrl + V 粘贴
     * - Ctrl + X 剪切
     * - Ctrl + Z 撤销
     * - Delete 删除
     * - Shift + 拖动 框选（已由 Selection 插件配置）
     * - Space + 拖动 移动画布（已由 panning 配置）
     * @returns {void}
     */
    const setupKeyboardShortcuts = () => {
      if (!graph.value) return

      // 全选：选中所有单元（节点与边），不改变原有“不可移动多选”的设定
      graph.value.bindKey('ctrl+a', () => {
        try {
          const cells = graph.value.getCells()
          if (cells && cells.length) {
            graph.value.cleanSelection()
            graph.value.select(cells)
          }
        } catch (e) {
        }
        return false
      })

      // 复制所选（排除开始/结束节点）
      graph.value.bindKey('ctrl+c', () => {
        try {
          const selected = graph.value.getSelectedCells()
          if (selected && selected.length) {
            const copyable = selected.filter(cell => {
              if (cell.isNode && cell.isNode()) {
                const t = cell.getData()?.type
                if (t === 'start' || t === 'end') return false
              }
              return true
            })
            if (copyable.length) {
              graph.value.copy(copyable)
            }
          }
        } catch (e) {
        }
        return false
      })

      // 剪切所选（跳过不可删除节点与开始/结束节点）
      graph.value.bindKey('ctrl+x', () => {
        try {
          const selected = graph.value.getSelectedCells()
          if (selected && selected.length) {
            const deletable = selected.filter(cell => {
              if (cell.isNode && cell.isNode()) {
                const d = cell.getData() || {}
                if (d.undeletable === true || d.type === 'start' || d.type === 'end') return false
              }
              return true
            })
            if (deletable.length) {
              graph.value.cut(deletable)
            }
          }
        } catch (e) {
        }
        return false
      })

      // 粘贴（并尝试维持泳道父子关系与开始/结束约束）
      graph.value.bindKey('ctrl+v', () => {
        try {
          const pasted = graph.value.paste({offset: 32}) || []
          // 重新选择粘贴出的单元
          graph.value.cleanSelection()
          graph.value.select(pasted)
          // 使节点在粘贴后根据几何位置嵌入泳道，并校验开始/结束节点约束
          pasted.forEach(cell => {
            try {
              if (cell && cell.isNode && cell.isNode()) {
                assignParentByGeometry(cell, true)
                enforceStartNodeConstraints(cell)
                enforceEndNodeConstraints(cell)
              }
            } catch (err) {
            }
          })
        } catch (e) {
        }
        return false
      })

      // 撤销
      graph.value.bindKey('ctrl+z', () => {
        try {
          graph.value.undo()
        } catch (e) {
        }
        return false
      })

      // 删除所选（跳过不可删除节点与开始/结束节点）
      graph.value.bindKey('delete', () => {
        try {
          const selected = graph.value.getSelectedCells()
          if (selected && selected.length) {
            selected.forEach(cell => {
              try {
                if (cell.isNode && cell.isNode()) {
                  const d = cell.getData() || {}
                  if (d.undeletable === true || d.type === 'start' || d.type === 'end') return
                  graph.value.removeNode(cell)
                } else if (cell.isEdge && cell.isEdge()) {
                  graph.value.removeEdge(cell)
                }
              } catch (err) {
              }
            })
          }
        } catch (e) {
        }
        return false
      })
    }

    /**
     * 在泳道内创建默认的“开启”节点，并设置为不可删除
     * @param {import('@antv/x6').Node} loop - 循环节点
     * @returns {void}
     */
    const createDefaultStartInLoop = (loop) => {
      try {
        if (!graph.value || !loop) return
        const children = loop.getChildren() || []
        const hasLoopStart = children.some(c => c.isNode && c.isNode() && (c.getData()?.type) === 'loop-start')
        if (hasLoopStart) return
        const lb = loop.getBBox()
        const hh = loop.getData()?.headerHeight ?? 30
        const pad = loop.getData()?.innerPadding ?? 8
        const startNode = nodeFactory.createNode('loop-start', {name: '开始'}, graph.value)
        graph.value.addNode(startNode)
        const sx = lb.x + pad + 12
        const sy = lb.y + hh + pad + 12
        startNode.position(sx, sy)
        const d = startNode.getData() || {}
        startNode.setData({...d, undeletable: true, parentId: loop.id})
        loop.addChild(startNode)
      } catch (err) {
      }
    }

    /**
     * 强制校验开始节点的唯一性与不可删除属性
     * - 每个泳道内至多一个开始节点
     * - 画布循环外至多一个开始节点
     * - 开始节点标记为不可删除
     * @param {import('@antv/x6').Node} node - 新增或待校验的节点
     * @returns {void}
     */
    const enforceStartNodeConstraints = (node) => {
      try {
        if (!graph.value || !node || !node.isNode || !node.isNode()) return
        const data = node.getData() || {}
        if (data.type !== 'start') return
        if (data.undeletable !== true) {
          node.setData({...data, undeletable: true})
        }
        const starts = graph.value.getNodes().filter(n => (n.getData()?.type) === 'start')
        const others = starts.filter(s => s.id !== node.id)
        if (others.length > 0) {
          graph.value.removeNode(node)
          try {
            ElMessage.warning('画布仅允许一个开始节点')
          } catch (e) {
          }
        }
      } catch (e) {
      }
    }

    /**
     * 强制校验结束节点的唯一性与不可删除属性
     * - 每个泳道内至多一个结束节点
     * - 画布循环外至多一个结束节点
     * - 结束节点标记为不可删除
     * @param {import('@antv/x6').Node} node - 新增或待校验的节点
     * @returns {void}
     */
    const enforceEndNodeConstraints = (node) => {
      try {
        if (!graph.value || !node || !node.isNode || !node.isNode()) return
        const data = node.getData() || {}
        if (data.type !== 'end') return
        if (data.undeletable !== true) {
          node.setData({...data, undeletable: true})
        }
        const ends = graph.value.getNodes().filter(n => (n.getData()?.type) === 'end')
        const others = ends.filter(s => s.id !== node.id)
        if (others.length > 0) {
          graph.value.removeNode(node)
          try { ElMessage.warning('画布仅允许一个结束节点') } catch (e) {}
        }
      } catch (e) {
      }
    }

    /**
     * 设置从工具栏拖拽到画布的监听
     * @returns {void}
     *
     * 行为说明：
     * - 读取拖拽类型，转换坐标并创建节点
     * - 若落点在泳道内，则在创建时建立父子关系（允许嵌入）
     */
    const setupDragAndDrop = () => {
      if (!container.value) return

      const containerEl = container.value

      // 阻止默认行为以允许拖拽
      containerEl.addEventListener('dragover', (event) => {
        event.preventDefault()
        event.dataTransfer.dropEffect = 'copy'
      })

      // 处理拖拽放置
      containerEl.addEventListener('drop', (event) => {
        event.preventDefault()

        // 获取拖拽的节点类型
        const nodeType = event.dataTransfer.getData('application/x-node-type')
        if (!nodeType) return

        if (nodeType === 'start') {
          const hasStart = graph.value.getNodes().some(n => (n.getData()?.type) === 'start')
          if (hasStart) {
            ElMessage.warning('画布仅允许一个开始节点')
            return
          }
        }

        if (nodeType === 'end') {
          const hasEnd = graph.value.getNodes().some(n => (n.getData()?.type) === 'end')
          if (hasEnd) {
            ElMessage.warning('画布仅允许一个结束节点')
            return
          }
        }

        // 将视口坐标直接转换为画布坐标，兼容缩放/平移
        const p = graph.value.clientToLocal({x: event.clientX, y: event.clientY})
        const x = p.x
        const y = p.y

        // 读取插件配置（若为插件类型）
        let createData = {}
        if (nodeType === 'plugin') {
          try {
            const payload = event.dataTransfer.getData('application/x-plugin-config')
            if (payload) {
              const cfg = JSON.parse(payload)
              createData = {
                name: cfg.label,
                iconUnicode: cfg.iconUnicode,
                color: cfg.color,
                fill: cfg.color
              }
            }
          } catch (e) {
          }
        }

        // 创建新节点
        const node = nodeFactory.createNode(nodeType, createData, graph.value)
        if (!node) return

        // 设置节点位置（居中于鼠标位置）
        const nodeSize = node.getSize()
        node.position(x - nodeSize.width / 2, y - nodeSize.height / 2)

        // 添加到画布
        graph.value.addNode(node)

        // 根据位置判断是否应当成为某个循环泳道的子节点（创建场景允许嵌入）
        assignParentByGeometry(node, true)

        // 若创建的是循环泳道，则自动在其内容区添加一个不可删除的“开启”节点
        if (nodeType === 'loop') {
          createDefaultStartInLoop(node)
        }

        // 选中新创建的节点
        const selection = graph.value.getPlugin('selection')
        if (selection && selection.select) {
          // 在连续拖拽创建多个节点时，清理之前的选中，避免一起拖动
          graph.value.cleanSelection()
          selection.select(node)
        }

        // 发出节点创建事件
        emit('node-created', node)
      })
    }

    /**
     * 根据几何位置为节点分配父泳道，并写入节点数据的 parentId 字段
     * 当节点不在任何泳道范围内时，清除其 parentId
     * @param {import('@antv/x6').Node} node - 需要分配父节点的子节点
     */
    /**
     * 根据几何位置为节点分配父泳道
     * @param {import('@antv/x6').Node} node - 需要分配父节点的子节点
     * @param {boolean} force - 是否允许从无父状态嵌入（仅用于拖拽创建场景）
     * @returns {void}
     *
     * 说明：
     * - 仅对普通节点生效（泳道本身不会嵌入）
     * - 建立父子关系后会将子节点位置校正至标题栏下方的内容区
     */
    const assignParentByGeometry = (node, force = false) => {
      if (!graph.value || !node) return
      if ((node.getData()?.type) === 'loop') return
      const loops = graph.value.getNodes().filter(n => (n.getData()?.type) === 'loop')
      if (loops.length === 0) {
        const d0 = node.getData() || {}
        node.setData({...d0, parentId: null})
        return
      }
      const childBBox = node.getBBox()
      const childCenter = {x: childBBox.x + childBBox.width / 2, y: childBBox.y + childBBox.height / 2}
      let matched = null
      for (let i = 0; i < loops.length; i += 1) {
        const lb = loops[i].getBBox()
        if (childCenter.x >= lb.x && childCenter.x <= lb.x + lb.width &&
            childCenter.y >= lb.y && childCenter.y <= lb.y + lb.height) {
          matched = loops[i]
          break
        }
      }
      const data = node.getData() || {}
      const hasParent = !!node.getParent()
      // 若节点已有父泳道，则不允许跨泳道重分配，仅在现有父泳道内容区内钳制位置
      if (hasParent) {
        const parent = node.getParent()
        if (parent && (parent.getData()?.type) === 'loop') {
          node.setData({...data, parentId: parent.id})
          const lb = parent.getBBox()
          const hh = parent.getData()?.headerHeight ?? 30
          const pad = parent.getData()?.innerPadding ?? 8
          const pos = node.getPosition()
          const minY = lb.y + hh + pad
          const minX = lb.x + pad
          const maxX = lb.x + lb.width - pad
          const ns = node.getSize()
          const maxY = lb.y + lb.height - pad - ns.height
          const clampedX = Math.min(Math.max(pos.x, minX), Math.max(minX, maxX - ns.width))
          const clampedY = Math.min(Math.max(pos.y, minY), Math.max(minY, maxY))
          if (pos.x !== clampedX || pos.y !== clampedY) {
            node.position(clampedX, clampedY)
          }
          return
        }
        // 无父泳道的场景继续按默认逻辑
      }
      // 仅在创建/粘贴等场景（force=true 且无父级）下允许按几何位置嵌入泳道
      if (!hasParent && matched && force) {
        node.setData({...data, parentId: matched.id})
        matched.addChild(node)
        const lb = matched.getBBox()
        const hh = matched.getData()?.headerHeight ?? 30
        const pad = matched.getData()?.innerPadding ?? 8
        const pos = node.getPosition()
        const minY = lb.y + hh + pad
        const minX = lb.x + pad
        const maxX = lb.x + lb.width - pad
        const ns = node.getSize()
        const maxY = lb.y + lb.height - pad - ns.height
        const clampedX = Math.min(Math.max(pos.x, minX), Math.max(minX, maxX - ns.width))
        const clampedY = Math.min(Math.max(pos.y, minY), Math.max(minY, maxY))
        if (pos.x !== clampedX || pos.y !== clampedY) {
          node.position(clampedX, clampedY)
        }
      } else if (!matched && !hasParent) {
        node.setData({...data, parentId: null})
      }
    }

    /**
     * 拖动时阻止无父节点进入任意泳道（保持在循环外部）
     * @param {import('@antv/x6').Node} node - 当前拖动节点
     * @returns {void}
     *
     * 说明：
     * - 拖拽开始于外部的节点如果进入泳道，会被立即回滚到拖拽起点
     * - 泳道自身不参与阻止逻辑（可自由拖动）
     */
    const preventEnterLoopOnDrag = (node) => {
      if (!graph.value || isReverting) return
      if ((node.getData()?.type) === 'loop') return
      const hadParentAtStart = dragStart.get(node.id)?.hadParent
      if (hadParentAtStart) return
      const loops = graph.value.getNodes().filter(n => (n.getData()?.type) === 'loop' && n.id !== node.id)
      if (loops.length === 0) return
      const bbox = node.getBBox()
      const intersectAny = loops.some(loop => rectsIntersect(bbox, loop.getBBox()))
      if (intersectAny) {
        const start = dragStart.get(node.id)
        if (start) {
          const current = node.getPosition()
          const dx = start.x - current.x
          const dy = start.y - current.y
          isReverting = true
          node.translate(dx, dy)
          isReverting = false
        }
      } else {
        const pos = node.getPosition()
        dragStart.set(node.id, {x: pos.x, y: pos.y, hadParent: false})
      }
    }

    /**
     * 防止循环节点之间相互重叠
     * @param {import('@antv/x6').Node} node - 当前拖动节点（循环）
     * @returns {void}
     */
    const preventLoopIntersectOnDrag = (node) => {
      if (!graph.value || isReverting) return
      if ((node.getData()?.type) !== 'loop') return
      const bbox = node.getBBox()
      // 获取所有其他循环节点
      const otherLoops = graph.value.getNodes().filter(n => n.id !== node.id && (n.getData()?.type) === 'loop')
      const intersecting = otherLoops.some(loop => rectsIntersect(bbox, loop.getBBox()))
      if (intersecting) {
        const start = dragStart.get(node.id)
        if (start) {
          const current = node.getPosition()
          const dx = start.x - current.x
          const dy = start.y - current.y
          isReverting = true
          node.translate(dx, dy)
          isReverting = false
        }
      }
    }

    /**
     * 防止循环泳道拖动时与外部节点发生相交
     * @param {import('@antv/x6').Node} node - 当前拖动节点（循环）
     * @returns {void}
     */
    const preventLoopOverlapOnDrag = (node) => {
      if (!graph.value || isReverting) return
      if ((node.getData()?.type) !== 'loop') return
      const bbox = node.getBBox()
      const children = node.getChildren() || []
      const childIds = new Set(children.map(c => c.id))
      // 获取所有非子节点的其他节点（不包括其他循环节点，因为有专门的函数处理）
      const others = graph.value.getNodes().filter(n => n.id !== node.id && (n.getData()?.type) !== 'loop' && !childIds.has(n.id))
      const overlapping = others.some(n => rectsIntersect(bbox, n.getBBox()))
      if (overlapping) {
        const start = dragStart.get(node.id)
        if (start) {
          const current = node.getPosition()
          const dx = start.x - current.x
          const dy = start.y - current.y
          isReverting = true
          node.translate(dx, dy)
          isReverting = false
        }
      } else {
        const pos = node.getPosition()
        dragStart.set(node.id, {x: pos.x, y: pos.y, hadParent: false})
      }
    }

    // 更新历史状态
    const updateHistoryState = () => {
      if (!graph.value) return
      const history = graph.value.getPlugin('history')
      canUndo.value = history.canUndo()
      canRedo.value = history.canRedo()
    }
    /**
     * 判断指定端口是否已连接
     * @param {Object} node - 节点实例
     * @param {string} portId - 端口ID
     * @returns {boolean} 是否已连接
     */
    const isPortConnected = (node, portId) => {
      const edges = graph.value.getConnectedEdges(node)
      return edges.some(e => (
          (e.getSourceCellId() === node.id && e.getSourcePortId() === portId) ||
          (e.getTargetCellId() === node.id && e.getTargetPortId() === portId)
      ))
    }

    /**
     * 判断两个矩形是否相交（含边界接触）
     * @param {{x:number,y:number,width:number,height:number}} a - 矩形A
     * @param {{x:number,y:number,width:number,height:number}} b - 矩形B
     * @returns {boolean} 是否相交
     */
    const rectsIntersect = (a, b) => {
      if (!a || !b) return false
      return !(b.x > a.x + a.width ||
               b.x + b.width < a.x ||
               b.y > a.y + a.height ||
               b.y + b.height < a.y)
    }

    /**
     * 设置端口显隐
     * @param {Object} node - 节点实例
     * @param {string} portId - 端口ID
     * @param {boolean} visible - 是否可见
     */
    const setPortVisible = (node, portId, visible) => {
      node.setPortProp(portId, 'attrs/circle/style/visibility', visible ? 'visible' : 'hidden')
    }

    /**
     * 设置端口颜色
     * @param {Object} node - 节点实例
     * @param {string} portId - 端口ID
     * @param {string} color - 颜色
     */
    const setPortColor = (node, portId, color) => {
      node.setPortProp(portId, 'attrs/circle/fill', color)
      node.setPortProp(portId, 'attrs/circle/stroke', color)
    }

    /**
     * 设置端口显示与颜色
     * @param {Object} node - 节点实例
     * @param {string} portId - 端口ID
     * @param {boolean} visible - 是否可见
     * @param {string} [color] - 颜色
     */
    const setPortDot = (node, portId, visible, color) => {
      setPortVisible(node, portId, visible)
      if (color) setPortColor(node, portId, color)
    }

    /**
     * 在节点上显示/隐藏端口（悬停时显示，离开时仅保留已连接端口）
     * @param {Object} node - 节点实例
     * @param {boolean} show - 是否显示
     */
    const showNodePorts = (node, show) => {
      const ps = node.getPorts()
      for (let i = 0; i < ps.length; i += 1) {
        const id = ps[i].id
        if (show) {
          setPortVisible(node, id, true)
        } else {
          const connected = isPortConnected(node, id)
          setPortVisible(node, id, connected)
          setPortColor(node, id, COLOR_PORT_GRAY)
        }
      }
    }

    /**
     * 用端口ID在节点上执行回调
     * @param {string} cellId - 单元ID
     * @param {string} portId - 端口ID
     * @param {Function} fn - 回调函数
     */
    const withNodePort = (cellId, portId, fn) => {
      if (!cellId || !portId || !fn) return
      const cell = graph.value.getCellById(cellId)
      if (cell && cell.isNode()) fn(cell, portId)
    }

    // 加载数据
    const loadData = (data) => {
      if (!graph.value || !data) return

      graph.value.fromJSON(data)
      updateHistoryState()
      // 加载后统一校验开始节点约束
      enforceGraphStartConstraints()
      enforceGraphEndConstraints()
    }

    // 导出数据
    const exportData = () => {
      if (!graph.value) return {}
      return graph.value.toJSON()
    }

    /**
     * 扫描当前图谱，统一校验所有开始节点的唯一性与不可删除标记
     * @returns {void}
     */
    const enforceGraphStartConstraints = () => {
      try {
        if (!graph.value) return
        const nodes = graph.value.getNodes()
        const loops = nodes.filter(n => (n.getData()?.type) === 'loop')
        const starts = nodes.filter(n => (n.getData()?.type) === 'start')
        // 标记所有开始节点不可删除
        starts.forEach(s => {
          const d = s.getData() || {}
          if (d.undeletable !== true) s.setData({...d, undeletable: true})
        })
        // 每个泳道保留一个开始节点
        loops.forEach(loop => {
          const children = loop.getChildren() || []
          const loopStarts = children.filter(c => c.isNode && c.isNode() && (c.getData()?.type) === 'start')
          if (loopStarts.length > 1) {
            for (let i = 1; i < loopStarts.length; i += 1) {
              try {
                graph.value.removeNode(loopStarts[i])
              } catch (e) {
              }
            }
          }
        })
        // 循环外仅保留一个开始节点
        const outsideStarts = starts.filter(s => {
          const p = s.getParent()
          return !(p && p.isNode && p.isNode() && (p.getData()?.type) === 'loop')
        })
        if (outsideStarts.length > 1) {
          for (let i = 1; i < outsideStarts.length; i += 1) {
            try {
              graph.value.removeNode(outsideStarts[i])
            } catch (e) {
            }
          }
        }
      } catch (e) {
      }
    }

    /**
     * 扫描当前图谱，统一校验所有结束节点的唯一性与不可删除标记
     * @returns {void}
     */
    const enforceGraphEndConstraints = () => {
      try {
        if (!graph.value) return
        const ends = graph.value.getNodes().filter(n => (n.getData()?.type) === 'end')
        ends.forEach(s => {
          const d = s.getData() || {}
          if (d.undeletable !== true) s.setData({...d, undeletable: true})
        })
        if (ends.length > 1) {
          for (let i = 1; i < ends.length; i += 1) {
            try { graph.value.removeNode(ends[i]) } catch (e) {}
          }
          try { ElMessage.warning('画布仅允许一个结束节点，已移除多余') } catch (e) {}
        }
      } catch (e) {
      }
    }

    // 更新节点数据
    const updateNodeData = (node, data) => {
      nodeFactory.updateNodeData(node, data)
    }

    // 更新边数据
    const updateEdgeData = (edge, data) => {
      edgeFactory.updateEdgeData(edge, data)
    }

    // 删除节点
    const deleteNode = (node) => {
      if (!graph.value) return
      graph.value.removeNode(node)
    }

    // 删除边
    const deleteEdge = (edge) => {
      if (!graph.value) return
      graph.value.removeEdge(edge)
    }

    // 撤销
    const handleUndo = () => {
      if (!graph.value) return
      graph.value.undo()
    }

    // 重做
    const handleRedo = () => {
      if (!graph.value) return
      graph.value.redo()
    }

    // 放大
    const handleZoomIn = () => {
      if (!graph.value) return
      graph.value.zoom(0.1)
    }

    // 缩小
    const handleZoomOut = () => {
      if (!graph.value) return
      graph.value.zoom(-0.1)
    }

    // 适应画布
    const handleFitView = () => {
      if (!graph.value) return
      graph.value.zoomToFit()
    }

    // 居中
    const handleCenter = () => {
      if (!graph.value) return
      graph.value.centerContent()
    }

    // 保存
    const handleSave = () => {
      emit('save')
    }

    // 获取图形实例
    const getGraph = () => graph.value

    // 生命周期
    onMounted(() => {
      initGraph()
      if (props.workflowData) {
        loadData(props.workflowData)
      }
    })

    onUnmounted(() => {
      if (graph.value) {
        graph.value.dispose()
      }
    })

    // 监听数据变化
    watch(() => props.workflowData, (newData) => {
      loadData(newData)
    })

    return {
      container,
      zoom,
      canUndo,
      canRedo,
      loadData,
      exportData,
      updateNodeData,
      updateEdgeData,
      deleteNode,
      deleteEdge,
      handleUndo,
      handleRedo,
      handleZoomIn,
      handleZoomOut,
      handleFitView,
      handleCenter,
      handleSave,
      getGraph
    }
  }
})
</script>

<style scoped>
.workflow-canvas {
  position: relative;
  width: 100%;
  height: 100%;
  background: #f8f9fa;
}

.canvas-toolbar {
  position: absolute;
  top: 5px;
  left: 5px;
  right: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  background: #fff;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 10;
}

.toolbar-left,
.toolbar-center,
.toolbar-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

.zoom-level {
  min-width: 40px;
  text-align: center;
  font-size: 12px;
  color: #595959;
}

.x6-container {
  position: absolute;
  top: 60px;
  left: 5px;
  right: 5px;
  bottom: 5px;
  background: #fff;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.status-bar {
  position: absolute;
  bottom: 8px;
  left: 16px;
  right: 16px;
  padding: 8px 16px;
  background: #fff;
  border-radius: 4px;
  font-size: 12px;
  color: #8c8c8c;
  text-align: center;
}
</style>
